<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="DlyqT" id="DlyqT"><span data-lake-id="u8d0e93a1" id="u8d0e93a1">典型回答</span></h1>
  <p data-lake-id="u0655a02b" id="u0655a02b"><br></p>
  <p data-lake-id="u20791c7b" id="u20791c7b"><span data-lake-id="ud75a09ae" id="ud75a09ae">重复消费、重复下单的问题，主要的解决办法就是做好幂等，因为在分布式系统中，我们是没办法保证消息不会重新投递的，也没办法保证用户一定不会快速的点击两次进行下单。</span></p>
  <p data-lake-id="ue2684a51" id="ue2684a51"><span data-lake-id="u7647f947" id="u7647f947">​</span><br></p>
  <p data-lake-id="u1dcb17a2" id="u1dcb17a2"><span data-lake-id="u07edd64d" id="u07edd64d">所以，对于服务的提供者来说，我们需要在接口中做好</span><strong><span data-lake-id="uc6f2bb6f" id="uc6f2bb6f">幂等控制</span></strong><span data-lake-id="u3ec8f5e6" id="u3ec8f5e6">，来避免因为重复而导致的脏数据。</span></p>
  <p data-lake-id="u9681db26" id="u9681db26"><span data-lake-id="u0d045468" id="u0d045468">​</span><br></p>
  <p data-lake-id="u427b2f09" id="u427b2f09"><span data-lake-id="u073ebd33" id="u073ebd33">对于消息的重复消费问题，比较常见的解决方式，就是通过</span><strong><span data-lake-id="uf88ea62f" id="uf88ea62f">消息中定义的一个幂等号，来做防重判断</span></strong><span data-lake-id="ub34ba15b" id="ub34ba15b">。这个幂等号一般是约定好的一个业务字段，如果没有这样一个字段的话，也可以用消息中间件的msg_id来做幂等控制，但是可能存在一个情况，那就是发送者重复发送了多次消息，这就会导致几次消息的msg_id不一样，但是消息内容一致。所以，一般都需要在消息中约定一个唯一的幂等字段或者业务字段。</span></p>
  <p data-lake-id="u16c438ce" id="u16c438ce"><br></p>
  <p data-lake-id="ueb9f5d1f" id="ueb9f5d1f"><span data-lake-id="u8164a675" id="u8164a675">而对于重复下单的场景，这个幂等号应该怎么产生呢？有一个好的办法就是生成</span><strong><span data-lake-id="uecd79726" id="uecd79726">token</span></strong><span data-lake-id="uf74b74ed" id="uf74b74ed">，当用户每一次访问页面的时候，都向后端接口请求获取一个token，然后在之后本页面的操作中，都需要把这个token带过来。如果页面没有刷新，这个token应该是不变的。</span></p>
  <p data-lake-id="ub04a9e3c" id="ub04a9e3c"><span data-lake-id="ubf17281d" id="ubf17281d">​</span><br></p>
  <p data-lake-id="u9b8970d4" id="u9b8970d4"><span data-lake-id="ub7531bb8" id="ub7531bb8">有了这个token就可以用它来做防重控制，并且还能避免有人恶意的刷我们的接口。</span></p>
  <p data-lake-id="ufeade0ad" id="ufeade0ad"><span data-lake-id="u9a0613aa" id="u9a0613aa">​</span><br></p>
  <p data-lake-id="ub20d602f" id="ub20d602f"><span data-lake-id="uedb408ba" id="uedb408ba">在消息或者下单场景中，有了唯一的幂等字段之后，就可以基于</span><strong><span data-lake-id="uc06a59c1" id="uc06a59c1">一锁、二判、三更新</span></strong><span data-lake-id="ubd2ce919" id="ubd2ce919">来进行幂等控制了，详见：</span></p>
  <p data-lake-id="u90072a57" id="u90072a57"><span data-lake-id="u63920bf6" id="u63920bf6">​</span><br></p>
  <p data-lake-id="u63f9aea6" id="u63f9aea6"><br></p>
  <p data-lake-id="ua02247e5" id="ua02247e5"><br></p>
  <h1 data-lake-id="YxTYi" id="YxTYi"><span data-lake-id="u3a95ef48" id="u3a95ef48">扩展知识</span></h1>
  <p data-lake-id="u86af8fe0" id="u86af8fe0"><br></p>
  <h2 data-lake-id="FigR9" id="FigR9"><span data-lake-id="uc9238cf9" id="uc9238cf9">token验证</span></h2>
  <p data-lake-id="u99a26e34" id="u99a26e34"><br></p>
  <p data-lake-id="ubaca3894" id="ubaca3894"><span data-lake-id="u84df4b73" id="u84df4b73">使用Redis可以很方便地实现token的验证，并且可以让一个token只能用一次，具体的实现方式如下：</span></p>
  <p data-lake-id="u7098424f" id="u7098424f"><span data-lake-id="u98d22325" id="u98d22325">​</span><br></p>
  <pre lang="java"><code>
import redis.clients.jedis.Jedis;
import java.util.UUID;
 
public class RedisToken {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost");
        String token = UUID.randomUUID().toString();
        jedis.setex(token, 60 * 60, "1");
        System.out.println("Token: " + token);
        jedis.close();
    }
}

</code></pre>
  <p data-lake-id="ub59532d2" id="ub59532d2"><span data-lake-id="ub99e4204" id="ub99e4204">​</span><br></p>
  <p data-lake-id="ud3118f16" id="ud3118f16"><span data-lake-id="u2a4814ad" id="u2a4814ad">这里只展示了用UUID的方式，而在分布式场景中，如果要生成一个全局唯一的ID，有很多其他方案，这里就不展开介绍了，详见：</span></p>
  <p data-lake-id="u6468b88e" id="u6468b88e"><span data-lake-id="u23b13daa" id="u23b13daa">​</span><br></p>
  <p data-lake-id="u5cb85ab5" id="u5cb85ab5"><br></p>
  <p data-lake-id="uba954178" id="uba954178"><span data-lake-id="u48a1da38" id="u48a1da38">在生成了token之后，后续请求就可以校验这个token是否有效，并且确保只能用一次：</span></p>
  <p data-lake-id="uf6be8f24" id="uf6be8f24"><span data-lake-id="u293b7f4b" id="u293b7f4b">​</span><br></p>
  <pre lang="java"><code>
import redis.clients.jedis.Jedis;
 
public class RedisTokenVerify {
    public void verify(String token) {
        Jedis jedis = new Jedis("localhost");
        String value = jedis.get(token);
        if (value != null) {
            //删除token
            jedis.del(token);
            //校验成功
        } else {
            //校验失败
        }
        jedis.close();
    }
}

</code></pre>
  <p data-lake-id="ub72a68bc" id="ub72a68bc"><br></p>
  <p data-lake-id="ub3e00881" id="ub3e00881"><span data-lake-id="u305b9adc" id="u305b9adc">这是一个token校验的简单实现，但是这个逻辑存在一个问题，那就是如果高并发场景，可能会导致多个线程同时，导致token同时校验通过。要解决这个问题，有两个办法，第一个是把get和del放到一个事务中，或者用lua脚本，或者用分布式锁也可以。</span></p>
  <p data-lake-id="u7a7e00b8" id="u7a7e00b8"><span data-lake-id="u1443e6ed" id="u1443e6ed">​</span><br></p>
  <p data-lake-id="u757632f8" id="u757632f8"><span data-lake-id="u638fc467" id="u638fc467">如使用Redis的事务：</span></p>
  <p data-lake-id="ufc3d2937" id="ufc3d2937"><span data-lake-id="u77bc1f49" id="u77bc1f49">​</span><br></p>
  <pre lang="java"><code>
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
 
public class RedisTokenVerifyWithTransaction {
   public void verify(String token) {
        Jedis jedis = new Jedis("localhost");
        Transaction tx = jedis.multi();
        tx.get(token);
        tx.del(token);
        Object result = tx.exec().get(0);
        if (result != null) {
             //校验成功
        } else {
           //校验失败
        }
        jedis.close();
    }
}

</code></pre>
  <p data-lake-id="u72c8ab96" id="u72c8ab96"><br></p>
  <p data-lake-id="u58c10131" id="u58c10131"><span data-lake-id="u4c97018f" id="u4c97018f">​</span><br></p>
 </body>
</html>